// Behavior originally contributed by Chinajade.
//
// LICENSE:
// This work is licensed under the
//     Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// also known as CC-BY-NC-SA.  To view a copy of this license, visit
//      http://creativecommons.org/licenses/by-nc-sa/3.0/
// or send a letter to
//      Creative Commons // 171 Second Street, Suite 300 // San Francisco, California, 94105, USA.
//

#region Summary and Documentation
// DOCUMENTATION:
//      http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Custom_Behavior:_CollectThings
//
// QUICK DOX:
//      Collects items from mobs or objects when (right-click) 'interaction' is required.
//      Most useful for those type of quests where you blow something up,
//      then you have to collect the pieces.
//
//  Parameters (required, then optional--both listed alphabetically):
//      (***One or more of the following two attributes must be specified***)
//      MobIdN [REQUIRED if ObjectId is omitted]: Defines the mobs that drop the Items we're after.
//              N may be omitted, or any numeric value--multiple mobs are supported.
//      ObjectIdN [REQUIRED if MobId is omitted]: Defines the objects that drop the Items we're after.
//              N may be omitted, or any numeric value--mulitple objects are supported.
//
//      (This attribute is optional, but governs what other attributes are optional)
//      CollectUntil [Default: RequiredCountReached]: Defines the terminating condition for
//              this behavior.  Available options include:  NoTargetsInArea, RequiredCountReached, QuestComplete.
//              "Targets" means mobs or objects--whatever is dropping the items we're after.
//
//      (***These attributes may/may not be optional based on value of CollectUntil attribute***)
//      CollectItemCount [REQUIRED if CollectUntil=RequiredCountReached; Default: 1]:
//              represents the number of items we must collect for the behavior to terminate.
//      CollectItemId [REQUIRED if CollectUntil=NoTargetsInArea or RequiredCountReached; Default:none]:
//              Identifies the item we are collecting.  The only time this attribute may be omitted
//              is when we're collecting intangibles such as 'attitudes' or 'liberations' that
//              will complete the quest.
//      QuestId [REQUIRED if CollectUntil=QuestComplete; Default:none]:
//
//      (***These attibutes are completely optional***)
//      HuntingGroundRadius [Default: 120]: The range from the anchor location (i.e., X/Y/Z) location at which
//              targets (mobs or objects) will be sought.
//      IgnoreMobsInBlackspots [Default: false]: If true, mobs sitting in blackspotted areas will not be
//              considered as targets.
//      MobState [Default: DontCare]: Identifies the state in which the Mob must be to be considered
//              as a target.  The MobState only applies if the target is some form of NPC.  The MobState
//              Valid values are Alive/Dead/DontCare.
//      NonCompeteDistance [Default: 25]: If a player is within this distance of a target that looks
//              interesting to us, we'll ignore the target.  The assumption is that the player may
//              be going for the same target, and we don't want to draw attention.
//      PostInteractDelay [Default: 1500ms]: The number of milliseconds to wait after each interaction.
//              This is useful if the target requires time for the interaction to complete.
//              This value must be on the closed interval [0..61000].
//      QuestCompleteRequirement [Default:NotComplete]:
//      QuestInLogRequirement [Default:InLog]:
//              A full discussion of how the Quest* attributes operate is described in
//              http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Programming_Cookbook:_QuestId_for_Custom_Behaviors
//      X/Y/Z [Default: Toon's initial position]: Defines the anchor of a search area for
//              which targets (mobs or objects) will be sought.  The hunting ground is defined by
//              this value coupled with the CollectionDistance.
//
#endregion


#region Examples
// Exmaples:
// <CustomBehavior File="CollectThings" ...other args... >
//     <Hotspot Name="Cathedral Square fishing dock" X="4554.003" Y="-4718.743" Z="883.0464" StartPoint="true" />
//     <Hotspot Name="The Shady Lady" X="4578.725" Y="-4721.257" Z="882.8724" />
//     <Hotspot Name="The Blue Recluse" X="4584.166" Y="-4693.487" Z="882.7331" StartPoint="true" />
// </CustomBehavior>
//
#endregion


#region Usings

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Xml;
using System.Xml.Linq;

using CommonBehaviors.Actions;

using Honorbuddy.QuestBehaviorCore;

using Styx;
using Styx.Common;
using Styx.CommonBot;
using Styx.CommonBot.Frames;
using Styx.CommonBot.Profiles;
using Styx.Pathing;
using Styx.Plugins;
using Styx.TreeSharp;
using Styx.WoWInternals;
using Styx.WoWInternals.WoWObjects;
using Styx.WoWInternals.World;

using Action = Styx.TreeSharp.Action;
#endregion


namespace Honorbuddy.Quest_Behaviors.CollectThings
{
    [CustomBehaviorFileName(@"CollectThings")]
    public class CollectThings : CustomForcedBehavior
    {
        public enum CollectUntilType
        {
            NoTargetsInArea,
            RequiredCountReached,
            QuestComplete,
        }

        public enum MobStateType
        {
            Alive,
            Dead,
            DontCare,
        }

        public CollectThings(Dictionary<string, string> args)
            : base(args)
        {
            QBCLog.BehaviorLoggingContext = this;

            try
            {
                bool isCollectItemCountRequired = false;
                bool isCollectItemIdRequired = false;
                bool isQuestIdRequired = false;

                CollectUntil = GetAttributeAsNullable<CollectUntilType>("CollectUntil", false, null, null) ?? CollectUntilType.RequiredCountReached;
                if ((CollectUntil == CollectUntilType.NoTargetsInArea)
                    || (CollectUntil == CollectUntilType.RequiredCountReached))
                {
                    isCollectItemCountRequired = true;
                    isCollectItemIdRequired = true;
                }
                else if (CollectUntil == CollectUntilType.QuestComplete)
                { isQuestIdRequired = true; }

                CollectItemCount = GetAttributeAsNullable<int>("CollectItemCount", isCollectItemCountRequired, ConstrainAs.CollectionCount, null) ?? 1;
                CollectItemId = GetAttributeAsNullable<int>("CollectItemId", isCollectItemIdRequired, ConstrainAs.ItemId, null) ?? 0;
                HuntingGroundAnchor = GetAttributeAsNullable<Vector3>("", false, ConstrainAs.Vector3NonEmpty, null) ?? Me.Location;
                HuntingGroundRadius = GetAttributeAsNullable<double>("HuntingGroundRadius", false, new ConstrainTo.Domain<double>(1.0, 200.0), new[] { "CollectionDistance" }) ?? 120.0;
                IgnoreMobsInBlackspots = GetAttributeAsNullable<bool>("IgnoreMobsInBlackspots", false, null, null) ?? false;
                MobIds = GetNumberedAttributesAsArray<int>("MobId", 0, ConstrainAs.MobId, null);
                MobState = GetAttributeAsNullable<MobStateType>("MobState", false, null, null) ?? MobStateType.DontCare;
                NonCompeteDistance = GetAttributeAsNullable<double>("NonCompeteDistance", false, new ConstrainTo.Domain<double>(1.0, 150.0), null) ?? 25.0;
                ObjectIds = GetNumberedAttributesAsArray<int>("ObjectId", 0, ConstrainAs.ObjectId, null);
                PostInteractDelay = TimeSpan.FromMilliseconds(GetAttributeAsNullable<int>("PostInteractDelay", false, new ConstrainTo.Domain<int>(0, 61000), null) ?? 1500);
                RandomizeStartingHotspot = GetAttributeAsNullable<bool>("RandomizeStartingHotspot", false, null, null) ?? false;
                QuestId = GetAttributeAsNullable<int>("QuestId", isQuestIdRequired, ConstrainAs.QuestId(this), null) ?? 0;
                QuestRequirementComplete = GetAttributeAsNullable<QuestCompleteRequirement>("QuestCompleteRequirement", false, null, null) ?? QuestCompleteRequirement.NotComplete;
                QuestRequirementInLog = GetAttributeAsNullable<QuestInLogRequirement>("QuestInLogRequirement", false, null, null) ?? QuestInLogRequirement.InLog;


                // Semantic coherency --
                if ((MobIds.Count() <= 0) && (ObjectIds.Count() <= 0))
                {
                    QBCLog.Error("You must specify one or more MobId(s) or ObjectId(s)");
                    IsAttributeProblem = true;
                }

                if (HuntingGroundRadius < (NonCompeteDistance * 2))
                {
                    QBCLog.Error("The CollectionDistance (saw '{0}') must be at least twice the size"
                                + " of the NonCompeteDistance (saw '{1}').",
                                HuntingGroundRadius,
                                NonCompeteDistance);
                    IsAttributeProblem = true;
                }


                // Find the item name --
                ItemInfo itemInfo = ItemInfo.FromId((uint)CollectItemId);

                ItemName = (itemInfo != null) ? itemInfo.Name : string.Format("Item({0})", CollectItemId);


                // Sub-behaviors...
                _behavior_SwimBreath = new SwimBreathBehavior();
                _behavior_HuntingGround = new HuntingGroundBehavior(IsViableTarget,
                                                                    HuntingGroundAnchor,
                                                                    HuntingGroundRadius);
                _behavior_UnderwaterLooting = new UnderwaterLootingBehavior();
            }

            catch (Exception except)
            {
                // Maintenance problems occur for a number of reasons.  The primary two are...
                // * Changes were made to the behavior, and boundary conditions weren't properly tested.
                // * The Honorbuddy core was changed, and the behavior wasn't adjusted for the new changes.
                // In any case, we pinpoint the source of the problem area here, and hopefully it
                // can be quickly resolved.
                QBCLog.Exception(except);
                IsAttributeProblem = true;
            }
        }

        // DON'T EDIT THIS--it is auto-populated by Git
        public override string VersionId => QuestBehaviorBase.GitIdToVersionId("$Id: 962c17efb14e61b87bf784a97475c1a382466da6 $");


        // Attributes provided by caller
        public int CollectItemCount { get; private set; }
        public int CollectItemId { get; private set; }
        public CollectUntilType CollectUntil { get; private set; }
        public Vector3 HuntingGroundAnchor { get; private set; }
        public double HuntingGroundRadius { get; private set; }
        public bool IgnoreMobsInBlackspots { get; private set; }
        public int[] MobIds { get; private set; }
        public MobStateType MobState { get; private set; }
        public double NonCompeteDistance { get; private set; }
        public int[] ObjectIds { get; private set; }
        public TimeSpan PostInteractDelay { get; private set; }
        public bool RandomizeStartingHotspot { get; private set; }
        public int QuestId { get; private set; }
        public QuestCompleteRequirement QuestRequirementComplete { get; private set; }
        public QuestInLogRequirement QuestRequirementInLog { get; private set; }

        // Private properties and data...
        private HuntingGroundBehavior _behavior_HuntingGround;
        private SwimBreathBehavior _behavior_SwimBreath;
        private UnderwaterLootingBehavior _behavior_UnderwaterLooting;
        private bool _isBehaviorDone = false;
        private PluginContainer _pluginAntiDrown;
        private bool _pluginAntiDrownWasEnabled;

        private WoWObject CurrentTarget { get { return (_behavior_HuntingGround.CurrentTarget); } }
        private readonly TimeSpan _delay_MobConsumedExpiry = TimeSpan.FromMinutes(7);
        private readonly TimeSpan _delay_BlacklistPlayerTooClose = TimeSpan.FromSeconds(90);
        private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
        private string ItemName { get; set; }
        private static LocalPlayer Me { get { return (StyxWoW.Me); } }

        // Private LINQ queries..
        private int CollectedItemCount
        {
            get
            {
                return ((int)Me.BagItems
                            .Where(item => (item.ItemInfo.Id == CollectItemId))
                            .Sum(item => item.StackCount));
            }
        }

        // If player is close to a target that is interesting to us, ignore the target...
        // The player may be going for the same mob, and we don't want to draw attention.
        // We'll blacklist the mob for a bit, in case the player is running around, or following
        // us.  The excaption is ithe player is in our party, then we can freely kill any target
        // close to him.
        private bool BlacklistIfPlayerNearby(WoWObject target)
        {
            WoWUnit nearestCompetingPlayer = ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
                                                    .OrderBy(player => player.Location.Distance(target.Location))
                                                    .FirstOrDefault(player => player.IsPlayer
                                                                                && player.IsAlive
                                                                                && !player.IsInOurParty());

            // If player is too close to the target, ignore target for a bit...
            if ((nearestCompetingPlayer != null)
                && (nearestCompetingPlayer.Location.Distance(target.Location) <= NonCompeteDistance))
            {
                target.LocallyBlacklist(_delay_BlacklistPlayerTooClose);
                return (true);
            }

            return (false);
        }


        private void GuiShowProgress(string completionReason)
        {
            TreeRoot.GoalText = string.Format("{0}: {1}/{2} {3}", this.GetType().Name, CollectedItemCount, CollectItemCount, ItemName);

            if (completionReason != null)
            {
                QBCLog.DeveloperInfo("Behavior done (" + completionReason + ")");
                TreeRoot.GoalText = string.Empty;
                TreeRoot.StatusText = string.Empty;
            }
        }


        public bool IsViableTarget(WoWObject target)
        {
            bool isViable;

            if (target == null)
            { return (false); }

            isViable = (target.IsValid
                        && (MobIds.Contains((int)target.Entry) || ObjectIds.Contains((int)target.Entry))
                        && !target.IsLocallyBlacklisted()
                        && !BlacklistIfPlayerNearby(target)
                        && Query.IsStateMatch_IgnoreMobsInBlackspots(target, IgnoreMobsInBlackspots));

            if (isViable && (target is WoWUnit))
            {
                WoWUnit wowUnit = target.ToUnit();

                isViable = ((wowUnit.IsAlive && (MobState == MobStateType.Alive))
                            || (wowUnit.IsDead && (MobState == MobStateType.Dead))
                            || (MobState == MobStateType.DontCare));
            }

            return (isViable);
        }


        private List<Vector3Named> ParseHuntingGroundHotspots()
        {
            List<Vector3Named> huntingGroundHotspots = new List<Vector3Named>();

            foreach (XElement element in Element.Elements().Where(elem => (elem.Name == "Hotspot")))
            {
                double? x = ParseXmlElementDouble(element, "X", true);
                double? y = ParseXmlElementDouble(element, "Y", true);
                double? z = ParseXmlElementDouble(element, "Z", true);

                if (!x.HasValue || !y.HasValue || !z.HasValue)
                { continue; }

                bool isStarting = ParseXmlElementBool(element, "StartPoint", false) ?? false;
                string name = ParseXmlElementString(element, "Name", false);

                huntingGroundHotspots.Add(new Vector3Named(new Vector3((float)x.Value, (float)y.Value, (float)z.Value), name, isStarting));
            }

            return huntingGroundHotspots;
        }


        private bool? ParseXmlElementBool(XElement element,
                                            string attributeName,
                                            bool isRequired)
        {
            string location = (((IXmlLineInfo)element).HasLineInfo()
                                        ? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
                                        : string.Empty);
            bool tmpBool;

            if (element.Attribute(attributeName) == null)
            {
                if (isRequired)
                {
                    QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
                    IsAttributeProblem = true;
                }
                return (null);
            }

            if (!bool.TryParse(element.Attribute(attributeName).Value, out tmpBool))
            {
                QBCLog.Error("Hotspot{0} '{1}' attribute is malformed", location, attributeName);
                IsAttributeProblem = true;
                return (null);
            }

            return (tmpBool);
        }


        private double? ParseXmlElementDouble(XElement element,
                                              string attributeName,
                                              bool isRequired)
        {
            string location = (((IXmlLineInfo)element).HasLineInfo()
                                        ? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
                                        : string.Empty);
            double tmpDouble;

            if (element.Attribute(attributeName) == null)
            {
                if (isRequired)
                {
                    QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
                    IsAttributeProblem = true;
                }
                return (null);
            }

            if (!double.TryParse(element.Attribute(attributeName).Value, NumberStyles.Float, CultureInfo.InvariantCulture, out tmpDouble))
            {
                QBCLog.Error("Hotspot{0} '{1}' attribute is malformed", location, attributeName);
                IsAttributeProblem = true;
                return (null);
            }

            return (tmpDouble);
        }


        private string ParseXmlElementString(XElement element,
                                              string attributeName,
                                              bool isRequired)
        {
            string location = (((IXmlLineInfo)element).HasLineInfo()
                                        ? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
                                        : string.Empty);

            if (element.Attribute(attributeName) == null)
            {
                if (isRequired)
                {
                    QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
                    IsAttributeProblem = true;
                }
                return (null);
            }

            return (element.Attribute(attributeName).Value);
        }


        #region Overrides of CustomForcedBehavior

        protected override Composite CreateBehavior()
        {
            return (
                new PrioritySelector(
                    // If behavior done, bail...
                    // Note that this is also an implicit "is quest complete" exit criteria, also.
                    new Decorator(ret => IsDone,
                        new Action(delegate { GuiShowProgress("quest complete"); })),

                    // If we've filled our inventory quota, we're done...
                    new Decorator(
                        ret => (CollectedItemCount >= CollectItemCount),
                        new Action(delegate
                            {
                                GuiShowProgress(string.Format("{0}/{1} items collected", CollectedItemCount, CollectItemCount));
                                _isBehaviorDone = true;
                            })),

                    // If we're dead, the behavior can't function so bail until alive...
                    new Decorator(ret => Me.IsDead, new ActionAlwaysSucceed()),

                    // If swimming, check if we need breath...
                    _behavior_SwimBreath.CreateBehavior(),

                    // If there is loot to clean up...
                    new Decorator(context => LootTargeting.LootMobs,
                        _behavior_UnderwaterLooting.CreateBehavior(() => true)),

                    // Find next target...
                    _behavior_HuntingGround.CreateBehavior_SelectTarget(() => (CollectUntil == CollectUntilType.NoTargetsInArea)),

                    // If no target and that's our exit criteria, we're done...
                    new Decorator(ret => ((CurrentTarget == null)
                                          && (CollectUntil == CollectUntilType.NoTargetsInArea)),
                        new Action(delegate
                        {
                            GuiShowProgress("No more objects/mobs in area");
                            _isBehaviorDone = true;
                        })),

                    // Otherwise, keep the unit of interest targeted...
                    new Decorator(ret => ((Me.CurrentTarget != CurrentTarget) && (CurrentTarget is WoWUnit)),
                        new Action(delegate
                        {
                            CurrentTarget.ToUnit().Target();
                            return (RunStatus.Failure);     // Fall through
                        })),

                    // Keep progress updated...
                    new Action(delegate
                    {
                        GuiShowProgress(null);
                        return (RunStatus.Failure);     // Fall through
                    }),

                    // If we're not at target, move to it...
                    _behavior_HuntingGround.CreateBehavior_MoveToTarget(),

                    // We're within interact range, collect the object...
                    new Sequence(
                        new Action(delegate { WoWMovement.MoveStop(); }),
                        new Action(delegate { _behavior_HuntingGround.MobEngaged(CurrentTarget); }),
                        new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
                        new Action(delegate { CurrentTarget.Interact(); }),
                        new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
                        new WaitContinue(PostInteractDelay, ret => false, new ActionAlwaysSucceed()),
                        new Action(delegate { CurrentTarget.LocallyBlacklist(_delay_MobConsumedExpiry); }),
                        new Action(delegate { Me.ClearTarget(); })
                        )
                )
            );
        }

        public override void OnFinished()
        {
            TreeRoot.GoalText = string.Empty;
            TreeRoot.StatusText = string.Empty;
            if (_pluginAntiDrown != null)
            {
                _pluginAntiDrown.Enabled = _pluginAntiDrownWasEnabled;
                _pluginAntiDrown = null;
            }
            base.OnFinished();
        }

        public override bool IsDone
        {
            get
            {
                return (_isBehaviorDone     // normal completion
                        || !UtilIsProgressRequirementsMet(QuestId, QuestRequirementInLog, QuestRequirementComplete));
            }
        }


        public override void OnStart()
        {
            // The XML element didn't exist when the constructor was called...
            // So we had to defer some final parsing that really should've happened in the constructor
            // to the OnStart() method.  This will parse the final arguments, and set IsAttributeProblem
            // correctly, for normal processing.
            var huntingGroundHotspots = ParseHuntingGroundHotspots();

            // This reports problems, and stops BT processing if there was a problem with attributes...
            // We had to defer this action, as the 'profile line number' is not available during the element's
            // constructor call.
            OnStart_HandleAttributeProblem();

            // If the quest is complete, this behavior is already done...
            // So we don't want to falsely inform the user of things that will be skipped.
            if (!IsDone)
            {
                _behavior_HuntingGround.UseHotspots(huntingGroundHotspots, RandomizeStartingHotspot);

                // Disable the AntiDrown plugin if present, as it interferes with our anti-drown prevention...
                _pluginAntiDrown = PluginManager.Plugins.FirstOrDefault(plugin => (plugin.Name == "Anti-Drown"));
                if (_pluginAntiDrown != null)
                {
                    _pluginAntiDrownWasEnabled = _pluginAntiDrown.Enabled;
                    _pluginAntiDrown.Enabled = false;
                }

                this.UpdateGoalText(QuestId);

                GuiShowProgress(null);
            }
        }

        #endregion
    }


    public class Vector3Named
    {
        public Vector3Named(Vector3 location,
                             string name,
                             bool isStarting)
        {
            Location = location;
            Name = (!string.IsNullOrEmpty(name) ? name : location.ToString());
            IsStarting = isStarting;
        }


        public Vector3Named(Vector3 location,
                             string name)
            : this(location, name, false)
        {
            // empty
        }

        public Vector3Named(Vector3 location)
            : this(location, null, false)
        {
            // empty
        }

        public Vector3Named()
            : this(Vector3.Zero, null, false)
        {
            // empty
        }

        public Vector3 Location { get; set; }
        public string Name { get; set; }
        public bool IsStarting { get; set; }
    }


    #region Reusable behaviors
    // The behaviors in this section were designed to be efficient and robust.
    // The robustness results in some larger-than-normal, but swift code.
    // We also designed them to be reused in other behaviors--just copy, paste,
    // and call them as-needed.

    public class HuntingGroundBehavior
    {
        public delegate bool BehaviorFailIfNoTargetsDelegate();
        public delegate double DistanceDelegate();
        public delegate bool IsViableTargetDelegate(WoWObject target);
        public delegate Vector3Named LocationDelegate();
        public delegate void LoggerDelegate(string messageType, string format, params object[] args);
        public delegate WoWObject WoWObjectDelegate();


        public HuntingGroundBehavior(IsViableTargetDelegate isViableTarget,
                                     Vector3 huntingGroundAnchor,
                                     double collectionDistance)
        {
            CollectionDistance = collectionDistance;
            HuntingGroundAnchor = new Vector3Named(huntingGroundAnchor, "Hunting Ground Anchor", true);
            IsViableTarget = isViableTarget;

            // UseHotspots(null, false);
        }


        public void MobEngaged(WoWObject wowObject)
        {
            if (wowObject == CurrentTarget)
            { _currentTargetAutoBlacklistTimer.Stop(); }
        }


        // Public properties...
        public double CollectionDistance { get; private set; }
        public WoWObject CurrentTarget { get; private set; }
        public Vector3Named HuntingGroundAnchor { get; private set; }
        public IsViableTargetDelegate IsViableTarget { get; private set; }


        // Private properties & data...
        private const string AuraName_DruidAquaticForm = "Aquatic Form";
        private readonly TimeSpan _delay_AutoBlacklist = TimeSpan.FromMinutes(7);
        private readonly TimeSpan _delay_RepopWait = TimeSpan.FromMilliseconds(500);
        private readonly TimeSpan _delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(1000);
        private readonly TimeSpan _delay_WoWClientMovementThrottle = TimeSpan.FromMilliseconds(0);
        private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
        private static LocalPlayer Me { get { return (StyxWoW.Me); } }
        private const double MinDistanceToUse_DruidAquaticForm = 27.0;
        private int _spellId_DruidAquaticForm = 1066;
        private IEnumerable<WoWObject> ViableTargets()
        {
            return ObjectManager.GetObjectsOfType<WoWObject>(true, false)
                    .Where(target => IsViableTarget(target)
                                     && (target.Distance < CollectionDistance))
                    .OrderBy(target => target.PathTraversalCost());
        }

        private TimeSpan _currentTargetAutoBlacklistTime = TimeSpan.FromSeconds(1);
        private readonly Stopwatch _currentTargetAutoBlacklistTimer = new Stopwatch();
        private Queue<Vector3Named> _hotSpots = new Queue<Vector3Named>();
        private Vector3Named _huntingGroundWaitPoint = new Vector3Named(Vector3.Zero,
                                                                                            "Hunting Ground Wait Point");
        private readonly Stopwatch _repopWaitingTime = new Stopwatch();


        /// <summary>
        /// The created behavior was meant to be used in a PrioritySelector.
        /// It may also have uses inside other TreeSharp Composites.
        /// </summary>
        ///
        /// <returns>
        /// <para>* RunStatus.Failure, if current target is viable.
        /// It will also return Failure if no targets could be located and failIfNoTargets is true</para>
        /// <para>* RunStatus.Success, if acquiring a target (or waiting for them to repop)</para>
        /// </returns>
        ///
        public Composite CreateBehavior_SelectTarget()
        {
            return (CreateBehavior_SelectTarget(() => false));
        }

        public Composite CreateBehavior_SelectTarget(BehaviorFailIfNoTargetsDelegate failIfNoTargets)
        {
            return (
            new PrioritySelector(

                // If we haven't engaged the mob when the auto-blacklist timer expires, give up on it and move on...
                new Decorator(ret => ((Query.IsViable(CurrentTarget))
                                        && (_currentTargetAutoBlacklistTimer.Elapsed > _currentTargetAutoBlacklistTime)),
                    new Action(delegate
                    {
                        QBCLog.Warning("Taking too long to engage '{0}'--blacklisting", CurrentTarget.SafeName);
                        CurrentTarget.LocallyBlacklist(_delay_AutoBlacklist);
                        CurrentTarget = null;
                    })),


                // If we don't have a current target, select a new one...
                // Once we select a target, its 'locked in' (unless it gets blacklisted).  This prevents us
                // from running back and forth between two equidistant targets.
                new Decorator(ret => (!Query.IsViable(CurrentTarget)
                                      || CurrentTarget.IsLocallyBlacklisted()
                                      || !IsViableTarget(CurrentTarget)),
                    new PrioritySelector(context => CurrentTarget = ViableTargets().FirstOrDefault(),

                        // If we found next target, we're done...
                        new Decorator(ret => (CurrentTarget != null),
                            new Action(delegate
                            {
                                _huntingGroundWaitPoint.Location = Vector3.Zero;

                                if (CurrentTarget is WoWUnit)
                                { CurrentTarget.ToUnit().Target(); }

                                _currentTargetAutoBlacklistTime = CalculateAutoBlacklistTime(CurrentTarget);
                                _currentTargetAutoBlacklistTimer.Reset();
                                _currentTargetAutoBlacklistTimer.Start();
                            })),

                        // If we've exhausted mob/object supply in area, and we need to wait, do so...
                        new Decorator(ret => !failIfNoTargets(),

                            // Move back to hunting ground anchor --
                            new PrioritySelector(

                                // If we've more than one hotspot, head to the next one...
                                new Decorator(ret => (_hotSpots.Count() > 1),
                                    new Sequence(context => FindNextHotspot(),
                                        new Action(nextHotspot => TreeRoot.StatusText = "No targets--moving to "
                                                                                        + ((Vector3Named)nextHotspot).Name),
                                        CreateBehavior_InternalMoveTo(() => FindNextHotspot())
                                        )),

                                // We find a point 'near' our anchor at which to wait...
                                // This way, if multiple people are using the same profile at the same time,
                                // they won't be standing on top of each other.
                                new Decorator(ret => (_huntingGroundWaitPoint.Location == Vector3.Zero),
                                    new Action(delegate
                                        {
                                            _huntingGroundWaitPoint.Location = HuntingGroundAnchor.Location.FanOutRandom(CollectionDistance * 0.25);
                                            TreeRoot.StatusText = "No targets--moving to wait near " + _huntingGroundWaitPoint.Name;
                                            _repopWaitingTime.Reset();
                                            _repopWaitingTime.Start();
                                        })),

                                // Move to our selected random point...
                                new Decorator(ret => !Navigator.AtLocation(_huntingGroundWaitPoint.Location),
                                    CreateBehavior_InternalMoveTo(() => _huntingGroundWaitPoint)),

                                // Tell user what's going on...
                                new Sequence(
                                    new Action(delegate
                                        {
                                            TreeRoot.GoalText = this.GetType().Name + ": Waiting for Repops";
                                            TreeRoot.StatusText = "No targets in area--waiting for repops.  " + BuildTimeAsString(_repopWaitingTime.Elapsed);
                                        }),
                                    new WaitContinue(_delay_RepopWait, ret => false, new ActionAlwaysSucceed()))
                                ))
                        )),

                // Re-select target, if it was lost (perhaps, due to combat)...
                new Decorator(ret => ((CurrentTarget is WoWUnit) && (Me.CurrentTarget != CurrentTarget)),
                    new Action(delegate { CurrentTarget.ToUnit().Target(); }))
                ));
        }


        public Composite CreateBehavior_MoveNearTarget(WoWObjectDelegate target,
                                                          DistanceDelegate minRange,
                                                          DistanceDelegate maxRange)
        {
            return (
            new PrioritySelector(context => target(),

                // If we're too far from target, move closer...
                new Decorator(wowObject => (((WoWObject)wowObject).Distance > maxRange()),
                    new Sequence(
                        new DecoratorThrottled(_delay_StatusUpdateThrottle, ret => true,
                            new Action(wowObject =>
                            {
                                TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                                    ((WoWObject)wowObject).SafeName,
                                                                    ((WoWObject)wowObject).Distance);
                            })),

                        CreateBehavior_InternalMoveTo(() => new Vector3Named(target().Location))
                    )),

                // If we're too close to target, back up...
                new Decorator(wowObject => (((WoWObject)wowObject).Distance < minRange()),
                    new PrioritySelector(

                        // If backing up, make sure we're facing the target...
                        new Decorator(ret => Me.MovementInfo.MovingBackward,
                            new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid))),

                        // Start backing up...
                        new Action(wowObject =>
                        {
                            TreeRoot.StatusText = "Too close to \"" + ((WoWObject)wowObject).SafeName + "\"--backing up";
                            WoWMovement.MoveStop();
                            WoWMovement.Face(((WoWObject)wowObject).Guid);
                            WoWMovement.Move(WoWMovement.MovementDirection.Backwards);
                        })
                        )),

                // We're between MinRange and MaxRange, stop movement and face the target...
                new Decorator(ret => Me.IsMoving,
                    new Sequence(
                        new Action(delegate { WoWMovement.MoveStop(); }),
                        new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid)),
                        new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
                        new ActionAlwaysFail()      // fall through to next element
                        ))
            ));
        }


        public Composite CreateBehavior_MoveToTarget()
        {
            return (CreateBehavior_MoveToTarget(() => CurrentTarget));
        }


        public Composite CreateBehavior_MoveToTarget(WoWObjectDelegate target)
        {
            return (
            new PrioritySelector(context => target(),

                // If we 'pass by' the current hotspot on the way to the target, advance to next hotspot...
                // This prevents bot-like 'back tracking'.
                new Decorator(ret => (Me.Location.Distance(_hotSpots.Peek().Location) < (CollectionDistance / 2)),
                    new Action(delegate
                    {
                        // Rotate to the next hotspot in the list...
                        _hotSpots.Enqueue(_hotSpots.Peek());
                        _hotSpots.Dequeue();

                        return (RunStatus.Failure);     // Fall through to next
                    })),


                // If we're not at target, move to it...
                new Decorator(wowObject => (((WoWObject)wowObject).Distance > ((WoWObject)wowObject).InteractRange),
                    new Sequence(
                        new DecoratorContinueThrottled(_delay_StatusUpdateThrottle, ret => true,
                            new Action(wowObject =>
                            {
                                TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                                    ((WoWObject)wowObject).SafeName,
                                                                    ((WoWObject)wowObject).Distance);
                            })),

                        CreateBehavior_InternalMoveTo(() => new Vector3Named(target().Location))
                    )),


                // Update status...
                new Action(wowObject =>
                    {
                        TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                            ((WoWObject)wowObject).SafeName,
                                                            ((WoWObject)wowObject).Distance);
                        return (RunStatus.Failure);
                    })
            ));
        }


        private static string BuildTimeAsString(TimeSpan timeSpan)
        {
            string formatString = string.Empty;

            if (timeSpan.Hours > 0)
            { formatString = "{0:D2}h:{1:D2}m:{2:D2}s"; }
            else if (timeSpan.Minutes > 0)
            { formatString = "{1:D2}m:{2:D2}s"; }
            else
            { formatString = "{2:D2}s"; }

            return (string.Format(formatString, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds));
        }


        private TimeSpan CalculateAutoBlacklistTime(WoWObject wowObject)
        {
            double pathDist = wowObject.SurfacePathDistance();

            float speed = Me.IsSwimming ? Me.MovementInfo.SwimmingForwardSpeed : Me.MovementInfo.RunSpeed;

            double dist = double.IsNaN(pathDist) ? wowObject.Distance : pathDist;
            var timeToWowObject = dist / speed;

            timeToWowObject *= 2.5;     // factor of safety
            timeToWowObject = Math.Max(timeToWowObject, 20.0);  // 20sec hard lower-limit

            return (TimeSpan.FromSeconds(timeToWowObject));
        }


        private Composite CreateBehavior_InternalMoveTo(LocationDelegate locationDelegate)
        {
            return (
            new Sequence(context => locationDelegate(),

                // Druids, switch to Aquatic Form if swimming and distance dictates...
                new DecoratorContinue(ret => (SpellManager.CanCast(_spellId_DruidAquaticForm)
                                                && !Me.HasAura(AuraName_DruidAquaticForm)
                                                && (Me.Location.Distance(locationDelegate().Location) > MinDistanceToUse_DruidAquaticForm)),
                    new Action(delegate { SpellManager.Cast(_spellId_DruidAquaticForm); })),

                // Move...
                new Action(delegate
                {
                    // Try to use Navigator to get there...
                    Vector3Named destination = locationDelegate();
                    MoveResult moveResult = Navigator.MoveTo(destination.Location);

                    // If Navigator fails, fall back to click-to-move...
                    if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
                    { WoWMovement.ClickToMove(destination.Location); }
                }),

                new WaitContinue(_delay_WoWClientMovementThrottle, ret => false, new ActionAlwaysSucceed())
                )
            );
        }


        private Vector3Named FindStartingHotspot(bool randomStartingHotspot)
        {
            List<Vector3Named> hotspotsByDistance =
                _hotSpots.OrderBy(hotspot => hotspot.Location.Distance(Me.Location)).ToList();

            List<Vector3Named> hotspotsStarting =
                hotspotsByDistance.Where(hotspot => hotspot.IsStarting).ToList();

            if (!hotspotsStarting.Any())
            {
                hotspotsStarting = hotspotsByDistance;
                QBCLog.DeveloperInfo("No explicit starting hotspot(s)--considering all");
            }

            QBCLog.DeveloperInfo("Hotspot count: {0} ({1})", hotspotsByDistance.Count(),
                        (randomStartingHotspot ? "randomized" : "starting at nearest"));

            Vector3 startingLocation = (randomStartingHotspot
                                                ? hotspotsStarting[StyxWoW.Random.Next(hotspotsStarting.Count)].Location
                                                : hotspotsStarting.First().Location);

            // Rotate the hotspot queue such that the nearest hotspot is on top...
            while (_hotSpots.Peek().Location != startingLocation)
            { _hotSpots.Enqueue(_hotSpots.Dequeue()); }

            QBCLog.DeveloperInfo("Starting hotspot is {0}", _hotSpots.Peek().Name);

            return (_hotSpots.Peek());
        }


        private Vector3Named FindNextHotspot()
        {
            Vector3Named currentHotspot = _hotSpots.Peek();

            // If we haven't reached the current hotspot, it is still the 'next' one...
            if (!Navigator.AtLocation(currentHotspot.Location))
            { return (currentHotspot); }

            // Otherwise, rotate to the next hotspot in the list...
            _hotSpots.Enqueue(_hotSpots.Dequeue());

            return (_hotSpots.Peek());
        }


        public void UseHotspots(IEnumerable<Vector3Named> hotspots,
                                        bool randomizeStartingHotspot)
        {
            _hotSpots = new Queue<Vector3Named>(hotspots ?? new Vector3Named[0]);

            if (_hotSpots.Count() <= 0)
            { _hotSpots.Enqueue(HuntingGroundAnchor); }

            FindStartingHotspot(randomizeStartingHotspot);
        }
    }


    /// <summary>
    /// <para>This behavior moves to fill a toon's lungs when needed.  It utilizes skills available
    /// such as a Warlock's Unending Breath, or a Druid's Aquatic Form in its calculations.
    /// The behavior looks for nearby air sources (underwater vents, water's surface, etc), and
    /// bases its time to move upon the distance that needs to be traveled to get to the air source
    /// and the toon's movement speed in the water.</para>
    /// <para>This behavior will *not* use engineering and other devices that may allow you to
    /// breath--as this could interfere with AutoEquip-type plugins.  This behavior will also
    /// *not* use potions to help you breathe, as that is a 'profile-level' decision that should
    /// be made.  In other words, you wouldn't want this behavior to burn a potion to collect
    /// a handful of object from shallow, but swimmable water.  The profile would know the expected
    /// duration underwater, but there is no way for this behavior to know such information.</para>
    /// </summary>
    //
    // Usage:
    //  private SwimBreathBehavior   _swimBreathBehavior  = new SwimBreathBehavior();
    //  ...
    //  new PrioritySelector(
    //      _swimBreathBehavior.CreateBehavior(),
    //
    public class SwimBreathBehavior
    {
        public delegate void LoggerDelegate(string messageType, string format, params object[] args);


        public SwimBreathBehavior()
        {
            // empty
        }


        // Private properites & data...
        private const string AuraName_DruidAquaticForm = "Aquatic Form";
        private const string AuraName_WarlockUnendingBreath = "Unending Breath";
        private int BreathTimeRemaining
        {
            get
            {
                return ((Timer_SwimBreath.IsVisible)
                       ? (int)Timer_SwimBreath.CurrentTime
                       : int.MaxValue);
            }
        }
        private readonly TimeSpan _delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(3000);
        private int _minTime_DruidBreath = 30000;    // in milliseconds
        private int _minTime_WarlockBreath = 30000;    // in milliseconds
        private LocalPlayer Me { get { return (StyxWoW.Me); } }
        private int _spellId_DruidAquaticForm = 1066;
        private int _spellId_WarlockUnendingBreath = 5697;
        private readonly TimeSpan _throttleTimer_BreathCheck = TimeSpan.FromSeconds(5);
        private readonly TimeSpan _throttleTimer_WarlockBreath = TimeSpan.FromSeconds(30);
        private readonly TimeSpan _timer_AuraRefresh_EnduringBreath = TimeSpan.FromMilliseconds(180000);
        private MirrorTimerInfo Timer_SwimBreath { get { return (Me.GetMirrorTimerInfo(MirrorTimerType.Breath)); } }
        private int[] _underwaterAirSourceObjectIds = { 177524 /* bubbly fissure */
																		  };

        private Composite _behaviorRoot;
        private bool _isSwimBreathNeeded;
        private AirSource _nearestAirSource;

        // Private structures...
        private struct AirSource
        {
            public Vector3 Location;
            public string Name;

            public AirSource(Vector3 location, string name) { Location = location; Name = name; }
            public double Distance { get { return (Location.Distance(StyxWoW.Me.Location)); } }
            public static AirSource Empty = new AirSource(Vector3.Zero, "NONE");
        }

        // Private LINQs
        private IEnumerable<WoWObject> UnderwaterAirSources
        {
            get
            {
                return (
                    ObjectManager.GetObjectsOfType<WoWObject>(true, false)
                .OrderBy(target => Me.Location.Distance(target.Location))
                .Where(target => _underwaterAirSourceObjectIds.Contains((int)target.Entry))
                    );
            }
        }


        private TimeSpan AuraTimeLeft(string auraName)
        {
            WoWAura wowAura = Me.GetAuraByName(auraName);

            return ((wowAura != null) ? wowAura.TimeLeft : TimeSpan.Zero);
        }


        /// <summary>
        /// The created behavior was meant to be used in a PrioritySelector.
        /// It may also have uses inside other TreeSharp Composites.
        /// </summary>
        ///
        /// <returns>
        /// <para>* RunStatus.Failure, if swim breath is not needed</para>
        /// <para>* RunStatus.Success, if we're catching our breath, or moving for it</para>
        /// </returns>
        ///
        public Composite CreateBehavior()
        {
            return (_behaviorRoot ?? (_behaviorRoot =
                new Decorator(ret => Me.IsSwimming,
                    new PrioritySelector(

                        // Moving to, or fetching breath...
                        new Decorator(ret => _isSwimBreathNeeded,
                            new PrioritySelector(

                                // If toon is filling lungs, stay put until full...
                                new Decorator(ret => ((Timer_SwimBreath.ChangePerMillisecond > 0)
                                                      && (Timer_SwimBreath.CurrentTime < Timer_SwimBreath.MaxValue)),
                                    new Action(delegate
                                    {
                                        WoWMovement.MoveStop();
                                        TreeRoot.StatusText = "Waiting for full breath";
                                    })),

                                // If lungs are full, back to work...
                                new Decorator(ret => (Timer_SwimBreath.CurrentTime >= Timer_SwimBreath.MaxValue),
                                    new Action(delegate { _isSwimBreathNeeded = false; })),

                                // Move toon to air source, if needed...
                                new Decorator(ret =>
                                    {
                                        _nearestAirSource = GetNearestAirSource();
                                        return !Navigator.AtLocation(_nearestAirSource.Location);
                                    },
                                    new Sequence(
                                        new DecoratorContinueThrottled(_delay_StatusUpdateThrottle, ret => true,
                                            new Action(delegate
                                            {
                                                TreeRoot.StatusText = string.Format("Moving to {0} for breath. (distance {1:0.0})",
                                                                                    _nearestAirSource.Name,
                                                                                    _nearestAirSource.Distance);
                                            })),

                                        new Action(delegate { UnderwaterMoveTo(_nearestAirSource.Location); })
                                        )
                                    )
                            )),


                        // If we're a Warlock, refresh Unending Breath as needed...
                        new DecoratorThrottled(_throttleTimer_WarlockBreath,
                            ret => (SpellManager.CanCast(_spellId_WarlockUnendingBreath)
                                    && (AuraTimeLeft(AuraName_WarlockUnendingBreath) <= _timer_AuraRefresh_EnduringBreath)),
                            new Action(delegate { SpellManager.Cast(_spellId_WarlockUnendingBreath); })),


                        // If time to breathe, do something about it...
                        new DecoratorThrottled(_throttleTimer_BreathCheck,
                            ret => IsBreathNeeded(),
                            new PrioritySelector(

                                // If we're a Druid, switch to Aquatic form for breath...
                                new Decorator(ret => (SpellManager.CanCast(_spellId_DruidAquaticForm)
                                                      && !Me.HasAura(AuraName_DruidAquaticForm)),
                                    new Action(delegate
                                    {
                                        SpellManager.Cast(_spellId_DruidAquaticForm);
                                        TreeRoot.StatusText = "Switching to Aquatic Form for breath";
                                        _isSwimBreathNeeded = true;
                                    })),


                                // Otherwise, we need to deal with 'normal' way to catch breath...
                                new Action(delegate
                                {
                                    _nearestAirSource = GetNearestAirSource();
                                    QBCLog.Info("Moving to {0} for breath. (distance {1:0.0})",
                                                    _nearestAirSource.Name, _nearestAirSource.Distance);
                                    _isSwimBreathNeeded = true;
                                })
                            ))
                    ))));
        }


        private AirSource GetNearestAirSource()
        {
            // Assume water's surface is nearest breath...
            AirSource nearestAirSource = new AirSource(Me.Location.WaterSurface(), "water's surface");
            WoWObject underwaterAirSource = UnderwaterAirSources.FirstOrDefault();

            // If underwater air source exists, and is closer that the water's surface...
            if ((underwaterAirSource != null)
                && (Me.Location.Distance(underwaterAirSource.Location) <= nearestAirSource.Distance))
            {
                nearestAirSource.Location = underwaterAirSource.Location;
                nearestAirSource.Name = underwaterAirSource.SafeName;
            }

            return (nearestAirSource);
        }


        private bool IsBreathNeeded()
        {
            int breathTimeRemaining = BreathTimeRemaining;

            if (Me.Class == WoWClass.Druid)
            { return (breathTimeRemaining < _minTime_DruidBreath); }

            else if (Me.Class == WoWClass.Warlock)
            { return (breathTimeRemaining < _minTime_WarlockBreath); }

            // Calculate time needed to get to an air source...
            AirSource airSource = GetNearestAirSource();
            double travelTime;

            travelTime = (((airSource.Location.Distance(Me.Location) / Me.MovementInfo.SwimmingForwardSpeed)
                          * 2.75)   // factor of safety
                          + (3 * _throttleTimer_BreathCheck.TotalSeconds));
            travelTime = Math.Min(travelTime, 30.0);    // Hard-minimum of 30secs
            travelTime *= 1000;     // to milliseconds

            return (breathTimeRemaining <= travelTime);
        }


        private void UnderwaterMoveTo(Vector3 location)
        {
            // Try to use Navigator to get there...
            MoveResult moveResult = Navigator.MoveTo(location);

            // If Navigator fails, resort to click-to-move...
            if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
            { WoWMovement.ClickToMove(location); }
        }
    }


    /// <summary>
    /// This behavior is necessary since Honorbuddy is incapable of moving underwater.
    /// </summary>
    //
    // Usage:
    //  private UnderwaterLootingBehavior   _underwaterLootingBehavior  = new UnderwaterLootingBehavior();
    //  ...
    //  new PrioritySelector(
    //      _underwaterLootingBehavior.CreateBehavior(),
    //
    public class UnderwaterLootingBehavior
    {
        public delegate bool ForceLootDelegate();
        public delegate void LoggerDelegate(string messageType, string format, params object[] args);


        public UnderwaterLootingBehavior()
        {
            // empty
        }


        // Private properties & data...
        private const string AuraName_DruidAquaticForm = "Aquatic Form";
        private readonly TimeSpan _delay_BlacklistLootedMob = TimeSpan.FromMinutes(7);
        private readonly TimeSpan _delay_WaitForLootCleanup = TimeSpan.FromMilliseconds(5000);
        private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
        private readonly TimeSpan _delay_WowClientWaitForLootFrame = TimeSpan.FromSeconds(10);
        private static LocalPlayer Me { get { return (StyxWoW.Me); } }
        private int _spellId_DruidAquaticForm = 1066;

        private Composite _behaviorRoot;
        private WoWObject _currentTarget;

        // Private LINQ...
        private IEnumerable<WoWUnit> LootList
        {
            get
            {
                return (ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
                        .Where(target => (target.IsDead && target.Lootable
                                          && !target.IsLootingBlacklisted())));
            }
        }


        /// <summary>
        /// The created behavior was meant to be used in a PrioritySelector.
        /// It may also have uses inside other TreeSharp Composites.
        /// </summary>
        ///
        /// <returns>
        /// <para>* RunStatus.Failure, if looting is not needed</para>
        /// <para>* RunStatus.Success, if we're in the process of looting things</para>
        /// </returns>
        ///
        public Composite CreateBehavior(ForceLootDelegate forceLoot)
        {
            return (_behaviorRoot ?? (_behaviorRoot =
                new Decorator(ret => ((LootTargeting.LootMobs || forceLoot()) && (LootList.Any())),
                    new PrioritySelector(

                        // If we're swimming, we need to do loot cleanup for ourselves...
                        new Decorator(ret => (Me.IsSwimming || forceLoot()),
                            new PrioritySelector(context => _currentTarget = LootList.FirstOrDefault(),

                                // If not at nearest target, move to it...
                                new Decorator(ret => (_currentTarget.Distance > _currentTarget.InteractRange),
                                    new Action(delegate
                                    {
                                        TreeRoot.StatusText = string.Format("Moving to loot target '{0}' (distance {1})...",
                                                                            _currentTarget.SafeName,
                                                                            _currentTarget.Distance);
                                        UnderwaterMoveTo(_currentTarget.Location);
                                    })),

                                // Within interact range, so loot it...
                                // NOTE: that we have to locally blacklist looted targets.  They sometimes
                                // have unique (e.g., quest-starting type) items on them.  If we already have
                                // have such an item in our inventory, the target remains lootable, but there
                                // is nothing we can pick up from it.  The blacklist prevents us from getting
                                // into silly loops because of such mechanics.
                                new Sequence(
                                    new Action(delegate { WoWMovement.MoveStop(); }),
                                    new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
                                    new Action(delegate { _currentTarget.Interact(); }),
                                    new WaitContinue(_delay_WowClientWaitForLootFrame,
                                                     ret => LootFrame.Instance.IsVisible,
                                                     new ActionAlwaysSucceed()),
                                    new DecoratorContinue(ret => LootFrame.Instance.IsVisible,
                                        new Action(ret => LootFrame.Instance.LootAll())),
                                    new Action(delegate { _currentTarget.LootingBlacklist(_delay_BlacklistLootedMob); })
                                    )
                            )),

                        // We're not swimming, so we want to wait for the 'normal' looting behavior
                        // to scoop up the loot before allowing other behaviors to continue...
                        // This keeps it from taking a few steps towards next mob, only to go back to
                        // previous mob and loot it.  This technique can still fail if Honorbddy is slow to update
                        // infomation.  However, it shuts a lot of back-tracking.
                        new WaitContinue(_delay_WaitForLootCleanup, ret => false, new ActionAlwaysSucceed())
                    )
                )));
        }


        private void UnderwaterMoveTo(Vector3 location)
        {
            // If we're a Druid, use Aquatic Form...
            if (SpellManager.CanCast(_spellId_DruidAquaticForm) && !Me.HasAura(AuraName_DruidAquaticForm))
            { SpellManager.Cast(_spellId_DruidAquaticForm); }

            // Try to use Navigator to get there...
            MoveResult moveResult = Navigator.MoveTo(location);

            if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
            { WoWMovement.ClickToMove(location); }
        }
    }

    #endregion  // Reusable behaviors


    #region Extensions to HBcore

    public class DecoratorContinueThrottled : DecoratorContinue
    {
        public DecoratorContinueThrottled(TimeSpan throttleTime,
                                          CanRunDecoratorDelegate canRun,
                                          Composite composite)
            : base(canRun, composite)
        {
            _throttleTime = throttleTime;

            _throttle = new Stopwatch();
            _throttle.Reset();
            _throttle.Start();
        }


        protected override bool CanRun(object context)
        {
            if (_throttle.Elapsed < _throttleTime)
            { return (false); }

            _throttle.Reset();
            _throttle.Start();

            return (base.CanRun(context));
        }


        private Stopwatch _throttle;
        private TimeSpan _throttleTime;
    }


    public class DecoratorThrottled : Decorator
    {
        public DecoratorThrottled(TimeSpan throttleTime,
                                  CanRunDecoratorDelegate canRun,
                                  Composite composite)
            : base(canRun, composite)
        {
            _throttleTime = throttleTime;

            _throttle = new Stopwatch();
            _throttle.Reset();
            _throttle.Start();
        }


        protected override bool CanRun(object context)
        {
            if (_throttle.Elapsed < _throttleTime)
            { return (false); }

            _throttle.Reset();
            _throttle.Start();

            return (base.CanRun(context));
        }


        private Stopwatch _throttle;
        private TimeSpan _throttleTime;
    }


    // The HBcore 'global' blacklist will also prevent looting.  We don't want that.
    // Since the HBcore blacklist is not built to instantiate, we have to roll our
    // own.  <sigh>
    public class LocalBlackList
    {
        public LocalBlackList(TimeSpan maxSweepTime)
        {
            _maxSweepTime = maxSweepTime;
            _stopWatchForSweeping.Start();
        }

        private Dictionary<WoWGuid, DateTime> _blackList = new Dictionary<WoWGuid, DateTime>();
        private TimeSpan _maxSweepTime;
        private Stopwatch _stopWatchForSweeping = new Stopwatch();


        public void Add(WoWGuid guid, TimeSpan timeSpan)
        {
            if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
            { RemoveExpired(); }

            _blackList[guid] = DateTime.Now.Add(timeSpan);
        }


        public bool Contains(WoWGuid guid)
        {
            if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
            { RemoveExpired(); }

            return (_blackList.ContainsKey(guid));
        }


        public void RemoveExpired()
        {
            DateTime now = DateTime.Now;

            List<WoWGuid> expiredEntries = (from key in _blackList.Keys
                                            where (_blackList[key] < now)
                                            select key).ToList();

            foreach (WoWGuid entry in expiredEntries)
            { _blackList.Remove(entry); }

            _stopWatchForSweeping.Reset();
            _stopWatchForSweeping.Start();
        }
    }


    public static class WoWObject_Extensions
    {
        private static LocalPlayer Me { get { return (StyxWoW.Me); } }

        // We provide our own 'local' blacklist.  If we use the global one maintained by HBcore,
        // that will prevent us from looting also.
        private static LocalBlackList s_blackList = new LocalBlackList(TimeSpan.FromSeconds(30));
        private static LocalBlackList s_blackListLooting = new LocalBlackList(TimeSpan.FromSeconds(30));

        public static void LocallyBlacklist(this WoWObject wowObject,
                                                 TimeSpan timeSpan)
        {
            s_blackList.Add(wowObject.Guid, timeSpan);
        }

        public static void LootingBlacklist(this WoWObject wowObject,
                                                 TimeSpan timeSpan)
        {
            s_blackListLooting.Add(wowObject.Guid, timeSpan);
        }


        public static bool IsLocallyBlacklisted(this WoWObject wowObject)
        {
            return (s_blackList.Contains(wowObject.Guid));
        }

        public static bool IsLootingBlacklisted(this WoWObject wowObject)
        {
            return (s_blackListLooting.Contains(wowObject.Guid));
        }
    }


    public static class WoWUnit_Extensions
    {
        private static LocalPlayer Me { get { return (StyxWoW.Me); } }

        public static bool IsInOurParty(this WoWUnit wowUnit)
        {
            return ((Me.PartyMembers.FirstOrDefault(partyMember => (partyMember.Guid == wowUnit.Guid))) != null);
        }
    }

    #endregion      // Extensions to HBcore
}

